/********************************************************************************
 * Copyright (c) 2011-2017 Red Hat Inc. and/or its affiliates and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 1.0 which is available at
 * http://www.eclipse.org/legal/epl-v10.html.
 *
 * SPDX-License-Identifier: EPL-1.0
 ********************************************************************************/
package org.eclipse.ceylon.ide.eclipse.core.launch;

import static org.eclipse.ceylon.model.typechecker.model.Module.DEFAULT_MODULE_NAME;
import static org.eclipse.ceylon.model.typechecker.model.Module.LANGUAGE_MODULE_NAME;
import static org.eclipse.ceylon.ide.eclipse.core.builder.CeylonBuilder.getPackage;
import static org.eclipse.ceylon.ide.eclipse.core.builder.CeylonBuilder.getProjectDeclaredSourceModules;
import static org.eclipse.ceylon.ide.eclipse.core.builder.CeylonBuilder.getProjectModules;
import static org.eclipse.ceylon.ide.eclipse.core.builder.CeylonBuilder.getProjectTypeChecker;
import static org.eclipse.ceylon.ide.eclipse.core.launch.ICeylonLaunchConfigurationConstants.ATTR_MODULE_NAME;
import static org.eclipse.ceylon.ide.eclipse.core.launch.ICeylonLaunchConfigurationConstants.ATTR_TOPLEVEL_NAME;
import static org.eclipse.ceylon.ide.eclipse.core.launch.ICeylonLaunchConfigurationConstants.CAN_LAUNCH_AS_CEYLON_JAVASCIPT_MODULE;
import static org.eclipse.ceylon.ide.eclipse.core.launch.ICeylonLaunchConfigurationConstants.CAN_LAUNCH_AS_CEYLON_JAVA_MODULE;
import static org.eclipse.ceylon.ide.eclipse.core.launch.ICeylonLaunchConfigurationConstants.DEFAULT_RUN_MARKER;
import static org.eclipse.ceylon.ide.eclipse.util.EditorUtil.getShell;
import static java.lang.Character.charCount;
import static java.lang.Character.isUpperCase;
import static org.eclipse.core.resources.ResourcesPlugin.getWorkspace;
import static org.eclipse.ceylon.ide.eclipse.java2ceylon.Java2CeylonProxies.*;


import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.window.Window;

import org.eclipse.ceylon.compiler.typechecker.TypeChecker;
import org.eclipse.ceylon.compiler.typechecker.context.PhasedUnit;
import org.eclipse.ceylon.ide.common.model.CeylonProject;
import org.eclipse.ceylon.ide.common.vfs.FileVirtualFile;
import org.eclipse.ceylon.model.typechecker.model.Class;
import org.eclipse.ceylon.model.typechecker.model.Declaration;
import org.eclipse.ceylon.model.typechecker.model.Function;
import org.eclipse.ceylon.model.typechecker.model.Module;
import org.eclipse.ceylon.model.typechecker.model.Modules;
import org.eclipse.ceylon.model.typechecker.model.Package;
import org.eclipse.ceylon.ide.eclipse.code.open.OpenDeclarationDialog;
import org.eclipse.ceylon.ide.eclipse.core.builder.CeylonBuilder;
import org.eclipse.ceylon.ide.eclipse.ui.CeylonPlugin;
import org.eclipse.ceylon.ide.eclipse.util.EditorUtil;

/**
 * This class is a stateless helper that groups together static utility methods
 */
public class LaunchHelper {

    static void addFiles(List<IFile> files, IResource resource) {
        switch (resource.getType()) {
            case IResource.FILE:
                IFile file = (IFile) resource;
                IPath path = file.getFullPath(); //getProjectRelativePath();
                if (path!=null && 
                        "ceylon".equals(path.getFileExtension()) ) {
                    files.add(file);
                }
                break;
            case IResource.FOLDER:
            case IResource.PROJECT:
                IContainer folder = (IContainer) resource;
                try {
                    for (IResource child: folder.members()) {
                        addFiles(files, child);
                    }
                }
                catch (CoreException e) {
                    e.printStackTrace();
                }
                break;
        }
    }
    
    static Object[] findDeclarationFromFiles(List<IFile> files) {
        List<Declaration> topLevelDeclarations = 
                new LinkedList<Declaration>();
        List<IFile> correspondingfiles = 
                new LinkedList<IFile>();
        for (IFile file : files) {
            IProject project = file.getProject();
            CeylonProject<IProject, IResource, IFolder, IFile> ceylonProject = modelJ2C().ceylonModel().getProject(project);
            TypeChecker typeChecker = 
                    getProjectTypeChecker(project);
            if (typeChecker != null) {
                FileVirtualFile<IProject, IResource, IFolder, IFile> virtualFile = 
                        vfsJ2C().createVirtualFile(file, ceylonProject.getIdeArtifact());
                PhasedUnit phasedUnit = 
                        typeChecker.getPhasedUnits()
                            .getPhasedUnit(virtualFile);
                if (phasedUnit!=null) {
                    List<Declaration> declarations = 
                            phasedUnit.getDeclarations();
                    for (Declaration d : declarations) {
                        if (isRunnable(d)) {
                            topLevelDeclarations.add(d);
                            correspondingfiles.add(file);
                        }
                    }
                }
            }
        }
        
        Declaration declarationToRun;
        IFile fileToRun;
        if (topLevelDeclarations.isEmpty()) {
            MessageDialog.openError(getShell(), 
                    "Ceylon Launcher", 
                    "No runnable function or class.\n(Only shared toplevel functions and classes with no parameters are runnable.)");
            return null;
        } 
        else if (topLevelDeclarations.size()>1) {
            declarationToRun = 
                    chooseDeclaration(topLevelDeclarations);
            if (declarationToRun!=null) {
                int index = topLevelDeclarations.indexOf(declarationToRun);
                fileToRun = correspondingfiles.get(index);
            }
            else {
                fileToRun = null;
            }
        } 
        else {
            declarationToRun = topLevelDeclarations.get(0);
            fileToRun = correspondingfiles.get(0);
        }
       
        return new Object[] {declarationToRun, fileToRun};
    }
    
    private static boolean isRunnable(Declaration d) {
        boolean candidateDeclaration = true;
        if (!d.isToplevel() || !d.isShared()) {
            candidateDeclaration = false;
        }
        if (d instanceof Function) {
            Function methodDecl = (Function) d;
            if (!methodDecl.getParameterLists().isEmpty() && 
                    !methodDecl.getParameterLists().get(0)
                            .getParameters().isEmpty()) {
                candidateDeclaration = false;
            }
        }
        else if (d instanceof Class) {
            Class classDecl = (Class) d;
            if (classDecl.isAbstract() || 
                    classDecl.getParameterList()==null || 
                    !classDecl.getParameterList()
                            .getParameters().isEmpty()) {
                candidateDeclaration = false;
            }
        }
        else {
            candidateDeclaration = false;
        }
        return candidateDeclaration;
    }
    
    static Module getModule(IProject project, String fullModuleName) {
        
        fullModuleName = 
                normalizeFullModuleName(fullModuleName);
        
        if (fullModuleName != null) {
            String[] parts = fullModuleName.split("/");
            
            if (parts != null && parts.length != 2) {
                return null;
            }
            
            for (Module module: 
                    getProjectDeclaredSourceModules(project)) {
                if (module.getNameAsString().equals(parts[0]) && 
                        module.getVersion().equals(parts[1])) {
                    return module;
                }
            }
            
            if (isDefaultModulePresent(project)) {
                return getDefaultModule(project);
            }
        }
        return null;
    }

    private static String normalizeFullModuleName(String fullModuleName) {
        if (DEFAULT_MODULE_NAME.equals(fullModuleName)) {
            return getFullModuleName(getEmptyDefaultModule());
        }
        else {
            return fullModuleName;
        }
    }

    private static Module getDefaultModule(IProject project) {
        Module defaultModule = 
                getProjectModules(project)
                    .getDefaultModule();
        if (defaultModule == null) {
            defaultModule = getEmptyDefaultModule();
        }
        return defaultModule;
    }
    
    private static Module getEmptyDefaultModule() {
        Module defaultModule = new Module();
        defaultModule.setName(Arrays.asList(DEFAULT_MODULE_NAME));
        defaultModule.setVersion("unversioned");
//        defaultModule.setDefault(true);
        return defaultModule;
    }
    
    static Module getModule(Declaration decl) {
        Package pack = decl.getUnit().getPackage();
        if (pack != null) {
            Module mod = pack.getModule();
            if (mod != null) {
                return mod;
            }
        }
        return getEmptyDefaultModule();
    }

    static String getModuleFullName(Declaration decl) {
        Module module = getModule(decl);
        if (module.isDefaultModule()) {
            return DEFAULT_MODULE_NAME;
        } else {
            return getFullModuleName(module);
        }
    }
 
    static String getModuleFullName(Module mod) {
        if (mod.isDefaultModule()) {
            return Module.DEFAULT_MODULE_NAME;
        } else {
            return getFullModuleName(mod);
        }
    }
    
   static Set<Module> getModules(IProject project, 
            boolean includeDefault) {
        Set<Module> modules = new HashSet<Module>();
        for(Module module: 
                getProjectDeclaredSourceModules(project)) {
            if (module.isAvailable() && 
                    !module.getNameAsString()
                            .startsWith(LANGUAGE_MODULE_NAME) && 
                    !module.isJava()) {
                if ((module.isDefaultModule() && includeDefault) // TODO : this is *never* true : the default module is not in the requested list
                        || (!module.isDefaultModule() && 
                                module.getPackage(module.getNameAsString())!=null)){
                    modules.add(module);
                }
            }
        }
        if (modules.isEmpty() || 
                isDefaultModulePresent(project)) {
            modules.add(getDefaultModule(project));
        }
        return modules;
    }
        
    private static boolean isDefaultModulePresent(IProject project) {
        Modules modules = getProjectModules(project);
        if (modules != null) {
            Module defaultModule = 
                    modules.getDefaultModule();
            if (defaultModule != null) {
                List<Declaration> decls = 
                        getDeclarationsForModule(project, 
                                defaultModule);
                if (!decls.isEmpty()) {
                    return true;
                }
            }
        }
        return false;
    }

    static boolean isModuleInProject(IProject project, 
            String fullModuleName) {
        if (fullModuleName.equals(Module.DEFAULT_MODULE_NAME) && 
                isDefaultModulePresent(project)) {
            return true;
        }
        
        for (Module module: getModules(project, false)) {
                String name = getFullModuleName(module);
                if (fullModuleName!=null &&
                        fullModuleName.equals(name)) {
                return true;
            }
        }
        return false;
    }

    static String getFullModuleName(Module module) {
        return module.getNameAsString() + "/" + 
                module.getVersion();
    }
 
    static List<Declaration> getDeclarationsForModule(IProject project, 
            Module module) {
        List<Declaration> modDecls = 
                new LinkedList<Declaration>();
        if (module != null) {
            List<Package> pkgs = module.getPackages(); // avoid concurrent exception
            for (Package pkg : pkgs) {
                if (pkg.getModule() != null && 
                        isPackageInProject(project, pkg))
                for (Declaration decl : pkg.getMembers()) {
                    if (isRunnable(decl)) {
                        modDecls.add(decl);
                    }                    
                }
            }
        }
        return modDecls;    
    }
    
    private static boolean isPackageInProject(IProject project, 
            Package pkg) {
        TypeChecker typeChecker = 
                getProjectTypeChecker(project);
        List<PhasedUnit> pus = 
                typeChecker.getPhasedUnits().getPhasedUnits();
        for (PhasedUnit phasedUnit : pus) {
            if (pkg.equals(phasedUnit.getPackage())) {
                return true;
            }
        }
        return false;
    }

    static List<Declaration> getDeclarationsForModule(String projectName, 
            String fullModuleName) {
        IProject project = getProjectFromName(projectName);
        Module module = getModule(project, fullModuleName);
        return getDeclarationsForModule(project, module);
    }
    
    /**
     * Does not attempt to get all declarations before it returns true 
     * @param project
     * @param fullModuleName
     * @param topLevelName
     * @return boolean if a top-level is contained in a module
     */
    static boolean isModuleContainsTopLevel(IProject project, 
            String fullModuleName, String topLevelName) {
        
        if (!isModuleInProject(project, fullModuleName)) {
            return false;
        }
        
        if (Module.DEFAULT_MODULE_NAME.equals(fullModuleName)) {
            fullModuleName = 
                    getFullModuleName(getDefaultModule(project));
        }
        
        Module mod = getModule(project, fullModuleName);
        
        if (mod == null) {
            return false;
        }
        
        for (Package pkg : mod.getPackages()) {
            for (Declaration decl : pkg.getMembers()) {
                if (getRunnableName(decl).equals(topLevelName)) {
                    return true;
                }
            }
        }
        
        return false;
    }

    static String getRunnableName(Declaration d) {
        return d.getQualifiedNameString().replace("::", ".");
    }

    static Declaration chooseDeclaration(final List<Declaration> decls) {
        OpenDeclarationDialog sd = 
                new OpenDeclarationDialog(false, false, 
                        EditorUtil.getShell(), 
                        "Ceylon Launcher",
                        "&Type part of a name, with wildcard *, or a camel hump pattern:",
                        "&Select a function to run:") {
            private static final String SETTINGS_ID = 
                    CeylonPlugin.PLUGIN_ID + ".selectRunnableDialog";            
            @Override
            protected String getFilterListAsString(String preference) {
                return "";
            }
            @Override
            public boolean enableDocArea() {
                return false;
            }
            @Override
            protected boolean addRunDecorations() {
                return true;
            }
            @Override
            protected IDialogSettings getDialogSettings() {
                IDialogSettings settings = 
                        CeylonPlugin.getInstance().getDialogSettings();
                IDialogSettings section = 
                        settings.getSection(SETTINGS_ID);
                if (section == null) {
                    section = settings.addNewSection(SETTINGS_ID);
                }
                return section;
            }
            @Override
            protected IDialogSettings getDialogBoundsSettings() {
                IDialogSettings settings = getDialogSettings();
                IDialogSettings section = 
                        settings.getSection(DIALOG_BOUNDS_SETTINGS);
                if (section == null) {
                    section = settings.addNewSection(DIALOG_BOUNDS_SETTINGS);
                    section.put(DIALOG_HEIGHT, 500);
                    section.put(DIALOG_WIDTH, 400);
                }
                return section;
            }
            @Override
            protected void fillViewMenu(IMenuManager menuManager) {}
            @Override
            protected ItemsFilter createFilter() {
                return new Filter() {
                    @Override
                    public String getPattern() {
                        String pattern = super.getPattern(); 
                        return pattern.isEmpty() ? "**" : pattern;
                    }
                };
            }
            @Override
            protected void fillContentProvider(
                    AbstractContentProvider contentProvider,
                    ItemsFilter itemsFilter,
                    IProgressMonitor monitor)
                    throws CoreException {
                for (int i=0; i<decls.size(); i++) {
                    DeclarationProxy item = 
                            new DeclarationProxy(decls.get(i));
                    contentProvider.add(item, itemsFilter);
                }
            }
        };

        if (sd.open() == Window.OK) {
            return (Declaration) sd.getFirstResult();
        }
        return null;
    }
    
    static Module chooseModule(IProject project, 
            boolean includeDefault) {
        CeylonModuleSelectionDialog cmsd = 
                new CeylonModuleSelectionDialog(getShell(), 
                        getModules(project, true)); 
        if (cmsd.open() == Window.OK) {
            return (Module)cmsd.getFirstResult();
        }
        return null;
    }
    
    static IProject getProjectFromName(String projectName) {
        if (projectName != null && projectName.length() > 0) {
            IWorkspace workspace = getWorkspace();
            IStatus status = 
                    workspace.validateName(projectName, 
                            IResource.PROJECT);
            if (status.isOK()) {
                return workspace.getRoot()
                        .getProject(projectName);
            }
        }
        return null;
    }

    static String getTopLevelNormalName(String moduleFullName, 
            String displayName) {
        if (displayName.contains(DEFAULT_RUN_MARKER) && 
                moduleFullName.indexOf('/') != -1) {
            return moduleFullName.substring(0, 
                    moduleFullName.indexOf('/')) + ".run";
        }
         
        return displayName;
    }
    
    static String getTopLevelDisplayName(Declaration decl) {
        String topLevelName = getRunnableName(decl);
        Module module = getModule(decl);
        if (module!=null && 
                decl.equals(getDefaultRunnableForModule(module))) {
            topLevelName = "run" + DEFAULT_RUN_MARKER; 
        }
        return topLevelName;
    }

    static Module getDefaultOrOnlyModule(IProject project, 
            boolean includeDefault) {
        Set<Module> modules = getModules(project, true);
        
        //if only one real module or just one default module, just send it back
        if (modules.size() == 1) {
            return modules.iterator().next();
        }
        
        if (modules.size() ==2 && !includeDefault) {
            Iterator<Module> modIterator = modules.iterator(); 
            while (modIterator.hasNext()) {
                Module realMod = modIterator.next();
                if (!realMod.isDefaultModule()) {
                    return realMod;
                }
            }
        }
        return null;
    }

    static Declaration getDefaultRunnableForModule(Module mod) {
        Declaration decl = null;
        if (mod.getRootPackage()!=null) {
            decl = mod.getRootPackage()
                    .getDirectMember("run", null, false);
        }
        return decl;
    }

    static Module getModule(IFolder folder) {
        Package pkg = getPackage(folder);
        if (pkg != null) {
            return pkg.getModule();
        }        
        return null;
    }
    
    static boolean isBuilderEnabled(IProject project, String property) {
        if (CAN_LAUNCH_AS_CEYLON_JAVA_MODULE.equals(property)) {
            return CeylonBuilder.compileToJava(project);
        }
        else if (CAN_LAUNCH_AS_CEYLON_JAVASCIPT_MODULE.equals(property)) {
            return CeylonBuilder.compileToJs(project);
        }
        return false;
    }

    public static String getTopLevel(ILaunchConfiguration configuration)
            throws CoreException {
        String topLevelName = 
                configuration.getAttribute(ATTR_TOPLEVEL_NAME,
                        (String) null);

        int def = -1;
        if (topLevelName != null) {
            def = topLevelName.indexOf(DEFAULT_RUN_MARKER);
        }
        
        if (def != -1) {
            topLevelName = null;
        }

        if (topLevelName == null) {
            String moduleName = 
                    configuration.getAttribute(ATTR_MODULE_NAME,
                            (String) null);
            if (moduleName != null) {
                String packageName = 
                        moduleName.replaceAll("/.*$", "");
                return packageName + ".run";
            }
        }
        return topLevelName;
    }

    public static String getStartLocation(ILaunchConfiguration configuration)
            throws CoreException {
        String location;
        String methodToStopIn = null;
        String toplevel = getTopLevel(configuration);
        if (toplevel != null) {
            int index = toplevel.lastIndexOf(".");
            if (index >= -1 && index < toplevel.length()-1) {
                int typeFirstChar = 
                        toplevel.codePointAt(index + 1);
                if (!isUpperCase(typeFirstChar)) {
                    // It's a top-level method
                    methodToStopIn = 
                            toplevel.substring(index + 
                                    charCount(typeFirstChar));
                    toplevel += "_";
                } else {
                    // It's a top-level class
                    methodToStopIn = "<init>"; // constructor
                }
            }
            location = toplevel + '/' + methodToStopIn;
        }
        else {
            location = null;
        }
        return location;
    }    
}
